package mc.support.schedule;

import mc.support.util.CronUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.Map;
import java.util.concurrent.*;

/**
 * Created with IntelliJ IDEA. <br/>
 * Author: Francis Yun    <br/>
 * Date: 2014-02-08  <br/>
 */
public class Timer {

    public static final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(10000);
    public static final ThreadPoolExecutor executor = new ThreadPoolExecutor(
            Runtime.getRuntime().availableProcessors() * 2,
            Runtime.getRuntime().availableProcessors() * 10,
            5, TimeUnit.MINUTES, queue,
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy() );

    public static final int CHECK_RANGE = 5;

    private static Logger log = LoggerFactory.getLogger( Timer.class );

    private Map<Integer, Map<String, Runnable>> tasks;
    private Map<String, Integer> points;
    private TimingDaemon timingDaemon;

    public Timer() {
        tasks = new ConcurrentHashMap<>();
        points = new ConcurrentHashMap<>();
        timingDaemon = new TimingDaemon();
    }

    public void start() {
        timingDaemon.start();
    }

    public void stop() {
        timingDaemon.cancel();
    }

    public String settle( Date d, Runnable t ) {
        String id = GUIDGenerator.generate();
        Integer point = CronUtil.toIdentificationInt( d );
        addTask( id, point, t );
        return id;
    }

    public void resettle(String id, Date d) {
        Runnable t = cancel( id );
        addTask( id, CronUtil.toIdentificationInt( d ), t );
    }

    public Runnable cancel(String id) {
        Integer executePoint = points.get( id );
        if ( null != executePoint ) {
            Map<String, Runnable> taskSet = tasks.get( executePoint );
            if ( null != taskSet ) {
                return taskSet.remove( id );
            }
        }
        return null;
    }

    private void addTask(String id, Integer executePoint, Runnable t) {
        points.put( id, executePoint );
        Map<String, Runnable> taskSet = tasks.get( executePoint );
        if ( null == taskSet ) {
            synchronized ( tasks ) {
                taskSet = tasks.get( executePoint );
                if ( null == taskSet ) {
                    taskSet = new ConcurrentHashMap<>();
                    tasks.put( executePoint, taskSet );
                }
            }
        }
        synchronized ( taskSet ) {
            taskSet.put( id, t );
        }
    }

    public void launch(Runnable r) {
        executor.submit( r );
    }

    private void doCheckAndLaunch() {
        Integer now = CronUtil.toIdentificationInt( new Date() );
        for ( int offset = 0; offset <= CHECK_RANGE; offset++ ) {
            checkAndLaunch( now - offset );
        }
    }

    private void checkAndLaunch(Integer point) {
        Map<String,Runnable> nowSet = tasks.remove( point );
        if ( null != nowSet && !nowSet.isEmpty() ) {
            synchronized ( nowSet ) {
                for ( Runnable t : nowSet.values() ) {
                    executor.submit( t );
                }
                nowSet.clear();
            }
        }
    }

    private class TimingDaemon extends Thread {

        private boolean keepOn = true;

        TimingDaemon() {
            setDaemon( true );
        }

        @Override
        public void run() {
            while ( keepOn ) {
                try {
                    doCheckAndLaunch();
                    TimeUnit.SECONDS.sleep( 1 );
                } catch (Exception e) {
                    //ignore
                    log.warn( "TimingDaemon exception [ type: "
                            + e.getClass().getName()
                            + ", msg: "
                            + e.getMessage() + "] "
                    );
                }
            }
        }

        public void cancel() {
            keepOn = false;
        }

    }

}
